import java.io.*;
import java.util.Scanner;
import java.util.Vector;
import java.lang.Math;
import java.math.BigDecimal;

public class Track {

	// Deceleration values. The double indicates the speed adjustment. Lower is better. Wall is crash.
	double wall = 1.0;
	double road = 0.01;
	double grass = 0.2;
	double mud = 0.4;
	
	//The array in which the deceleration values are stored. 
	//The location in the array is the surface indicator of the track
	//So if a cell of the track contains a '2', the deceleration is surface[2]=0.2
	private double[] surface = { wall, road, grass, mud };

	//Alternative output for surface indicators, previously used for ascii output
	private char[] surface_map = { 'x', '.', ',', '~' }; 
	
	//These two arrays are basically the track in surface indicators and deceleration values.
	private int[][] xy; //
	public double[][] doublexdoubly;

	private int[][] start_coords;		// Starting coordinates of the track.
	private int[][] checkpoints; 		// List of coordinates between which the
										// checkpoints are. The finish line is the
										// last checkpoint
	private int[][][] checkpointcells; 	// List of the lists of checkpointcells.
	private int[][] car_positions;		// Current positions of all cars.
	
	private String savedir = "tracks/"; // Folder where tracks are stored.
	

	// Manual initialization. Shouldn't be used, just added it for testing purposes
	Track(int[][] start, int[][] check, int[][] track) {

		start_coords = start;
		checkpoints = check;
		xy = track;
	}

	// Normal initialization. Given a string name, it loads a track from a text file.
	Track(String filename) throws IOException {
		loadTrack(filename);
	}

	// Returns starting coordinates
	public int[][] getStart() {
		return start_coords;
	}

	// Returning surface indicator. Notice that x and y seem swapped because
	// that's how they are stored in the array.
	public double getDecel(int x, int y) {
		if(x<0||y<0||y>=doublexdoubly.length||x>=doublexdoubly[0].length) return 1;
		else if(checkCar(x,y)) return 1;
		else {
			//System.out.print(doublexdoubly[y][x] + " ");
			return doublexdoubly[y][x];
		}
	}

	// Returns the track as an array of the surface deceleration values
	public double[][] getTrack() {
		return doublexdoubly;
	}

	// Returns the track as an array of the surface indicators
	public int[][] getIntTrack() {
		return xy;

	}

	// Returns the checkpoint cells
	public int[][][] getCheckpointCells() {
		return checkpointcells;
	}

	// Returns the checkpoints
	public int[][] getCheckpoints() {
		return checkpoints;
	}
	
	// Checks if there is a car at some coordinates
	private boolean checkCar(int xxx, int yyy) {
		for(int[] cood : car_positions) if(cood[0] == xxx && cood[1] == yyy) return true;
		return false;
	}
	
	//Change the current car positions
	public void setCarpositions(int[][] carpos){
		car_positions = carpos;		
	}

	
	//Returns a subarray of deceleration values of the track.
	//This is used for the relative view of the car.
	//It's always a square of size*size, rotated on the rotation around point x,y.
	public double[][] turnLine(int x, int y, int rotation, int size) {
		double[][] output = new double[size][size];	
		double dy, dx;
		int right=0,down=0;
		int tempx, tempy;

		if (rotation > 315 || rotation < 45) {

			dy = Math.tan(Math.toRadians(rotation));
			for (int i = y; i < y + size; i++) {
				double yy = i;
				for (int j = x; j < x + size; j++) {
					yy += dy;
					tempx = j;
					tempy = (int) Math.floor(yy);
					output[down][right++] = getDecel(tempx,tempy);
				}
				right=0;
				down++;
			}

		}

		else if (45 < rotation && rotation < 135) {
			dx = Math.tan(Math.toRadians(90 - rotation));
			for (int i = x ; i > x - size; i--) {
				double xx = i;

				for (int j = y; j < y + size ; j++) {
					xx += dx;
					tempx = (int) Math.floor(xx);
					tempy = j;
					output[down][right++] = getDecel(tempx,tempy);
					
					

				}
				right=0;
				down++;
			}

		}

		else if (135 < rotation && rotation < 225) {
			if (rotation == 180)
				dy = 0;
			else
				dy = Math.tan(Math.toRadians(rotation)) * -1;

			for (int i = y ; i > y - size; i--) {
				double yy = i;
				for (int j = x ; j > x - size; j--) {
					yy += dy;
					tempx = j;
					tempy = (int) Math.floor(yy);
					output[down][right++] = getDecel(tempx,tempy);
					

				}
				right=0;
				down++;
			}
		}

		else if (225 < rotation && rotation < 315) {
			dx = Math.tan(Math.toRadians(270 - rotation)) * -1;

			for (int i = x ; i < x + size; i++) {
				double xx = i;

				for (int j = y ; j > y - size; j--) {
					xx += dx;
					tempx = (int) Math.floor(xx);
					tempy = j;
					output[down][right++] = getDecel(tempx,tempy);

				}
				right=0;
				down++;
			}

		}
		
		else if(rotation==45 || rotation == 135 || rotation == 225 || rotation == 315) turnLine(x,y,rotation-1,size);
		return output;
	}

	
	
	// Returning a subarray of surfaces between two coordinates.
	// This was used for the view before it could rotate.
	public double[][] getSubArray(int x1, int y1, int x2, int y2) {
		double[][] subArray = new double[Math.abs(x2 - x1) + 1][Math.abs(y2
				- y1) + 1];
		if (x1 > x2) {
			int x3;
			x3 = x1;
			x1 = x2;
			x2 = x3;
		}

		if (y1 > y2) {
			int y3;
			y3 = y1;
			y1 = y2;
			y2 = y3;
		}

		for (int i = y1; i <= y2; i++) {
			for (int j = x1; j <= x2; j++) {
				if (j < 0 || i < 0 || i > xy.length - 1 || j > xy[0].length - 1) {
					subArray[i - y1][j - x1] = 0;
				} else
					subArray[i - y1][j - x1] = doublexdoubly[i][j];
			}
		}

		return subArray;
	}
	

	// Returns the number of checkpoints
	// Needed to check if a car passed the last checkpoint.
	public int getCheckpointsAmount() {
		return checkpointcells.length;
	}

	// Given a coordinate and the required checkpoint, it returns if this
	// coordinate lies on that checkpoint.
	public boolean checkPointPass(int x, int y, int checkpoint) {
		if (checkpoint < checkpointcells.length && checkpoint >= 0) {
			for (int cCell = 0; cCell < checkpointcells[checkpoint].length; cCell++) {
				if (checkpointcells[checkpoint][cCell][0] == x
						&& checkpointcells[checkpoint][cCell][1] == y)
					return true;
			}
		}

		return false;
	}
	

	// Returns a vector of cell coordinates on the lines between two coordinates.
	// Used to check which cells are on a checkpoint line, since a checkpoint is defined by 2 coordinates.
	private Vector<int[]> line2cells(int[] cell1, int[] cell2) {
		double x1, x2, y1, y2;
		int xdif = cell2[0] - cell1[0];
		int ydif = cell2[1] - cell1[1];
		Vector<int[]> output = new Vector<int[]>();

		if (xdif != 0) {
			double xvector = (double) ydif / (double) xdif;
			if (cell1[0] < cell2[0]) {
				x1 = cell1[0];
				x2 = cell2[0];
				y1 = cell1[1];
				y2 = cell2[1];
			} else {
				x1 = cell2[0];
				x2 = cell1[0];
				y1 = cell2[1];
				y2 = cell1[1];
			}
			while (!(x1 > x2)) {
				int[] tempcoords = { (int) Math.round(x1),
						(int) Math.floor(getScaled(y1, 3)) };
				if (!matchingCoords(output, tempcoords))
					output.add(tempcoords);
				y1 += xvector;
				x1++;
			}
		}

		if (ydif != 0) {
			double yvector = (double) xdif / (double) ydif;
			if (cell1[1] < cell2[1]) {
				x1 = cell1[0];
				y1 = cell1[1];
				x2 = cell2[0];
				y2 = cell2[1];
			} else {
				x1 = cell2[0];
				y1 = cell2[1];
				x2 = cell1[0];
				y2 = cell1[1];

			}

			while (!(y1 > y2)) {
				int[] tempcoords = { (int) Math.floor(getScaled(x1, 3)),
						(int) Math.round(y1) };
				if (!matchingCoords(output, tempcoords))
					output.add(tempcoords);
				x1 += yvector;
				y1++;
			}
		}

		return output;

	}

	
	// Checks if a coordinate is in a vector of coordinates
	private boolean matchingCoords(Vector<int[]> coordlist, int[] coord) {
		for (int i = 0; i < coordlist.size(); i++)
			if (coordlist.get(i)[0] == coord[0]
					&& coordlist.get(i)[1] == coord[1])
				return true;
		return false;

	}
	

	// Round off function used by line2cells. Returns a double rounded down to a
	// certain decimal, decided by int scale
	private double getScaled(double value, int scale) {
		double result = value; // default: unscaled
		result = new BigDecimal("" + value).setScale(scale,
				BigDecimal.ROUND_HALF_UP).doubleValue();

		return result;

	}

	// Prints a vector
	public void printVector(Vector<int[]> in) {
		System.out.print("Printing Vector! ");
		for (int i = 0; i < in.size(); i++) {
			System.out.print("(");
			for (int x : in.elementAt(i))
				System.out.print(+x + ",");
			System.out.print(") ");
		}
		System.out.println("Printed Vector!");

	}

	// Prints a track
	public void print_track() {
		for (int[] y : xy) {
			for (int x : y) {
				if (x >= 0 && x < surface.length)
					System.out.print(surface_map[x]);
				else
					System.out.print(x);
			}
			System.out.println();
		}
	}

	// Prints an array of int[]s
	public void print_r(int[] intarray) {
		for (int x : intarray)
			System.out.print(x + ";");
		// System.out.println();

	}

	// Prints an array of int[][]s
	public void print_r(int[][] intarray) {
		for (int[] x : intarray) {
			print_r(x);
			System.out.println();
		}

	}

	// Prints an array of double[][]s
	public void print_r(double[][] dubarray) {
		for (double[] x : dubarray) {
			for (double y : x)
				System.out.print(y + ";");
			System.out.println();
		}

	}

	// Prints an array of int[][][]s
	public void print_r(int[][][] intarray) {
		for (int[][] y : intarray) {
			for (int[] x : y) {
				System.out.print("(" + x[0] + "," + x[1] + ") ");
			}
			System.out.println();
		}

	}

	// Converts a vector to an integer array
	private int[] vec2ar(Vector<Integer> input) {
		int[] output = new int[input.size()];
		for (int i = 0; i < output.length; i++)
			output[i] = input.elementAt(i);
		return output;
	}

	// Converts a vector to an integer array array
	private int[][] vec2ar(Vector<int[]> input) {
		int[][] output = new int[input.size()][2];
		for (int i = 0; i < output.length; i++) {
			output[i] = input.elementAt(i);

		}
		return output;
	}

	// Converts a vector to an integer array array array
	private int[][][] vec2ar(Vector<int[][]> input) {
		int[][][] output = new int[input.size()][][];
		for (int i = 0; i < output.length; i++)
			output[i] = input.elementAt(i);
		return output;
	}

	// Converts a vector to an double array array
	private double[][] intar2doubar(int[][] input) {
		double[][] output = new double[input.length][input[0].length];
		for (int i = 0; i < input.length; i++) {
			for (int j = 0; j < input[i].length; j++) {
				output[i][j] = surface[input[i][j]];
			}
		}
		return output;

	}

	
	// Checks if the checkpoint coordinates are correct (not outside the map, no
	// missing coordinates)
	private boolean checkCheckpoints(int[][] coords) {
		if (coords.length % 2 != 0) {
			System.err.println("Missing a checkpoint");
			return false;
		} else {
			for (int[] coord : coords) {
				if (coord.length != 2 || coord[0] < 0
						|| coord[0] > xy[0].length || coord[1] < 0
						|| coord[1] > xy.length) {
					System.err.println("Erroneous checkpoint");
					return false;
				}
			}

		}
		return true;

	}

	
	// Converts the checkpoint coordinates to lines.
	private void setCheckpoints() {

		if (checkCheckpoints(checkpoints)) {
			Vector<int[][]> checkline = new Vector<int[][]>();
			for (int i = 0; i < checkpoints.length; i += 2)
				checkline.add(vec2ar(line2cells(checkpoints[i],
						checkpoints[i + 1])));
			checkpointcells = vec2ar(checkline);
		}

		else
			System.err
					.println("Faulty checkpoints, no checkpoints were processed");

	}
	

	// Ugly save and load functions. It's messy but it works and it's all we
	// need.
	// Tracks are saved in the tracks dir (can be changed with the constant
	// "savedir")
	// First the start coords are written, separated with spaces
	// Next line the same for the checkpoint coords
	// The rest of the file is used to write the track. See example track.
	private void loadTrack(String filename) throws IOException {
		int[] rowar;
		int b, c;

		File filetest = new File(savedir + filename);
		//System.out.println(filetest.getAbsolutePath());
		if (!filetest.exists()) {
			System.err.println("Filename \"" + filename + "\" not found");
		}
		FileReader file = new FileReader(savedir + filename);
		BufferedReader ifile = new BufferedReader(file);

		String coordsline = ifile.readLine();
		Scanner sc = new Scanner(coordsline);

		Vector<int[]> coords = new Vector<int[]>();
		Vector<Integer> coord = new Vector<Integer>();
		while (sc.hasNext()) {
			b = sc.nextInt();
			coord.add(b);
			b = sc.nextInt();
			coord.add(b);
			coords.add(vec2ar(coord));
			coord.clear();
		}
		start_coords = vec2ar(coords);
		coords.clear();
		coordsline = ifile.readLine();
		sc = new Scanner(coordsline);

		while (sc.hasNext()) {
			b = sc.nextInt();
			coord.add(b);
			b = sc.nextInt();
			coord.add(b);

			coords.add(vec2ar(coord));
			coord.clear();
		}
		checkpoints = vec2ar(coords);

		Vector<Integer> row = new Vector<Integer>();
		Vector<int[]> table = new Vector<int[]>();

		c = ifile.read();

		while (c != -1) {
			while (c > 47 && c < 58) {
				row.add(c - 48);
				c = ifile.read();
			}
			if (row.size() > 0) {
				rowar = vec2ar(row);
				table.add(rowar);
			}
			row.clear();
			c = ifile.read();
		}
		xy = vec2ar(table);
		doublexdoubly = intar2doubar(vec2ar(table));
		setCheckpoints();

		ifile.close();
		file.close();

	}

	private void saveTrack(String filename) throws IOException {
		FileWriter out = new FileWriter(savedir + filename);
		for (int i = 0; i < start_coords.length - 1; i++)
			out.write(start_coords[i][0] + " " + start_coords[i][1] + " ");
		out.write(start_coords[start_coords.length - 1][0] + " "
				+ start_coords[start_coords.length - 1][1]);
		out.write(13);
		out.write(10);
		for (int i = 0; i < checkpoints.length - 1; i++)
			out.write(checkpoints[i][0] + " " + checkpoints[i][1] + " ");
		out.write(checkpoints[checkpoints.length - 1][0] + " "
				+ checkpoints[checkpoints.length - 1][1]);
		out.write(13);
		out.write(10);
		for (int i = 0; i < xy.length - 1; i++) {
			for (int x : xy[i])
				out.write(x + 48);
			out.write(13);
			out.write(10);
		}
		for (int x : xy[xy.length - 1])
			out.write(x + 48);
		out.close();

	}

}
